package tacos.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//import org.springframework.security.core.authority.SimpleGrantedAuthority;
//import org.springframework.security.core.userdetails.User;
//import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
//import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import tacos.User;
import tacos.data.UserRepository;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

// Spring Security的基础配置类
@Configuration
// 使@PreAuthorize发挥作用，需要启用全局的方法安全功能。为了实现这一点，需要使用@EnableGlobalMethodSecurity注解标注安全配置类，如下所示：
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig {

    /*
    * BCryptPasswordEncoder，这是Spring Security所提供的如下多个密码转码器之一。
    * ●BCryptPasswordEncoder：使用bcrypt强哈希加密。
    * ●NoOpPasswordEncoder：不使用任何转码。
    * ●Pbkdf2PasswordEncoder：使用PBKDF2加密。
    * ●SCryptPasswordEncoder：使用Scrypt哈希加密。
    * ●StandardPasswordEncoder：使用SHA-256哈希加密。
    * 不管使用哪种密码转码器，都需要明白，数据库中的密码永远不会被解码。
    * 用户在登录时输入的密码将会使用相同的算法转码，并与数据库中已编码的密码进行对比。这种对比是在PasswordEncoder的matches()方法中进行的。
    * */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /*
     * Spring Security提供了多个内置的UserDetailsService实现，包括：●内存用户存储；●JDBC用户存储；●LDAP用户存储。
     * 在内存用户详情服务bean中声明用户。
     * 对于测试和非常简单的应用来讲，基于内存的用户详情服务是很有用的，但是这种方式不能很方便地编辑用户。
     * 如果需要新增、移除或变更用户，我们要对代码做出必要的修改，然后重新构建和部署应用。
     * */
//    @Bean
//    public UserDetailsService userDetailsService(PasswordEncoder encoder) {
//        List<UserDetails> usersList = new ArrayList<>();
//        usersList.add(new User(
//                "buzz", encoder.encode("password"),
//                Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"))));
//        usersList.add(new User(
//                "woody", encoder.encode("password"),
//                Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"))));
//        return new InMemoryUserDetailsManager(usersList);
//    }

    /*
    * tacos.service包下的UserDetailsService只定义了一个名为loadUserByUsername()的方法，这意味着它是一个函数式接口，能够以lambda表达式的方式来实现，
    * 从而避免提供一个完整的实现类。因为我们真正需要的是让自定义的UserDetailsService将用户查找的功能委托给UserRepository，所以可以使用如下配置方法简单地声明一个bean：
    * */
    @Bean
    public UserDetailsService userDetailsService(UserRepository userRepo) {
        return username -> {
            User user = userRepo.findByUsername(username);
            if (user != null) return user;

            throw new UsernameNotFoundException("User '" + username + "' not found");
        };
    }

    /*
    * 主页、登录页和注册页应该对未认证的用户开放。为了配置这些安全性规则，我们需要声明一个SecurityFilterChain bean。
    * 如下的@Bean方法展示了一个最小（但没有什么用）的SecurityFilterChain声明：
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.build();
    }
    * HttpSecurity对象，该对象会作为一个构造器，用来配置Web级别的安全问题处理方法。通过HttpSecurity对象设置安全配置之后，
    * 调用build()方法就能创建并从bean方法中返回一个SecurityFilterChain对象。
    * */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        /*
        * HttpSecurity可以配置很多的功能，其中包括：●要求在为某个请求提供服务之前，满足特定的安全条件；●配置自定义的登录页；●使用户能够退出应用；●预防跨站请求伪造。
        * HttpSecurity最常见的需求就是拦截请求以确保用户具备适当的权限。
        * */
        return http
                .authorizeRequests()
                .antMatchers("/design", "/orders").hasRole("USER")
//                .antMatchers("/design", "/orders").access("hasRole('USER') or hasRole('ADMIN')")
                .antMatchers("/", "/**").permitAll()
                // 为了替换内置的登录页，我们首先需要告诉Spring Security自定义登录页的路径。这可以通过调用HttpSecurity对象的formLogin()方法来实现，如下所示：
                .and().formLogin().loginPage("/login")
//                // 声明Spring Security要监听对“/authenticate”的请求来处理登录信息的提交。同时，用户名和密码的字段名应该是user和pwd。
//                .loginProcessingUrl("/authenticate").usernameParameter("user").passwordParameter("pwd")
//                // 如果用户登录成功，就会被重定向到“/design”路径。
//                .defaultSuccessUrl("/design", true)
                // 如果通过声明SecurityFilterChain bean来自定义安全配置，那么除了其他的安全配置，还需要启用OAuth2登录，如下所示：
                .and().oauth2Login()
                // 如果同时需要支持传统的通过用户名和密码登录，可以在配置中指定登录页，如下所示：
                .loginPage("/login")
                // 启用退出功能，拦截对“/logout”的请求，当用户退出的时候，会话会被清理。可以调用logoutSuccessUrl()指定退出后的页面，如下所示：
                .and().logout().logoutSuccessUrl("/")
                .and().build();
        /* 表5.1　用来定义如何保护路径的配置方法
| 方法                        | 功能                                                         |
| -------------------------- | ------------------------------------------------------------ |
| access(String)             | 如果给定的SpEL（代表Spring ExpressionLanguage）表达式计算结果为true，就允许访问 |
| anonymous()                | 允许匿名用户访问                                             |
| authenticated()            | 允许认证过的用户访问                                         |
| denyAll()                  | 无条件拒绝所有访问                                           |
| fullyAuthenticated()       | 如果用户进行了完整认证（而不是通过Rememberme功能认证的），就允许访问 |
| hasAnyAuthority(String...) | 如果用户具备给定权限中的某一个，就允许访问                   |
| hasAnyRole(String...)      | 如果用户具备给定角色中的某一个，就允许访问                   |
| hasAuthority(String)       | 如果用户具备给定权限，就允许访问                             |
| hasIpAddress(String)       | 如果请求来自给定IP，就允许访问                               |
| hasRole(String)            | 如果用户具备给定角色，就允许访问                             |
| not()                      | 对其他访问方法的结果求反                                     |
| permitAll()                | 无条件允许访问                                               |
| rememberMe()               | 如果用户是通过Remember-me功能认证的，就允许访问              |
         * 我们还可以使用access()方法，通过为其提供SpEL表达式来声明更丰富的安全规则。Spring Security扩展了SpEL，包含多个安全相关的值和函数，如表5.2所示。
         * 表5.2　Spring Security对Spring表达式语言的扩展
| 安全表达式                                                   | 计算结果                                                     |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| authentication                                               | 用户的认证对象                                               |
| denyAll                                                      | 结果始终为false                                              |
| hasAnyAuthority(String...authorities)                        | 如果用户被授予了给定权限中的任意一个，结果为true             |
| hasAnyRole(String.. roles)                                   | 如果用户具有给定角色中的任意一个，结果为true                 |
| hasAuthority(String authority)                               | 如果用户被授予了给定的权限，结果为true                       |
| hasPermission(Object target, Object permission)              | 如果用户能够访问特定目标对象以获取给定权限，结果为true       |
| hasPermission(Serializable targetId String targetType,Objeotpermission) | 如果用户能够访问targetId和targetType中给定的象以获取给定权限，结果为true |
| hasRole(String role)                                         | 如果用户被授予了给定的角色，结果为true                       |
| hasIpAddress(String ipAddress)                               | 如果请求来自给定IP，结果为true                               |
| isAnonymous()                                                | 如果用户为匿名用户，结果为true                               |
| isAuthenticated()                                            | 如果用户进行了认证，结果为true                               |
| isFullyAuthenticated()                                       | 如果用户进行了完整认证（而不是通过Renember-me功能认证的），结果为true |
| isRememberMe()                                               | 如果用户是通过Renemberme功能认证的，结果为true               |
| permitAll                                                    | 结果始终为true                                               |
| principal                                                    | 用户的principal对象
*/
    }
}
